<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<!--
 Licensed to the Apache Software Foundation (ASF) under one or more
 contributor license agreements.  See the NOTICE file distributed with
 this work for additional information regarding copyright ownership.
 The ASF licenses this file to You under the Apache License, Version 2.0
 (the "License"); you may not use this file except in compliance with
 the License.  You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
-->
<html><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Storm UI</title>
<link href="/css/bootstrap-3.3.1.min.css" rel="stylesheet" type="text/css">
<link href="/css/jquery.dataTables.1.10.4.min.css" rel="stylesheet" type="text/css">
<link href="/css/dataTables.bootstrap.css" rel="stylesheet" type="text/css">
<link href="/css/style.css?_ts=${packageTimestamp}" rel="stylesheet" type="text/css">
<script src="/js/jquery-1.11.1.min.js" type="text/javascript"></script>
<script src="/js/jquery.dataTables.1.10.4.min.js" type="text/javascript"></script>
<script src="/js/jquery.cookies.2.2.0.min.js" type="text/javascript"></script>
<script src="/js/jquery.mustache.js" type="text/javascript"></script>
<script src="/js/url.min.js" type="text/javascript"></script>
<script src="/js/bootstrap-3.3.1.min.js" type="text/javascript"></script>
<script src="/js/jquery.blockUI.min.js" type="text/javascript"></script>
<script src="/js/moment.min.js" type="text/javascript"></script>
<script src="/js/dataTables.bootstrap.min.js" type="text/javascript"></script>
<script src="/js/script.js?_ts=${packageTimestamp}" type="text/javascript"></script>
</head>
<body>
<div class="container-fluid">
  <div class="row">
    <div class="col-md-11">
      <h1><a href="/">Storm UI</a></h1>
    </div>
    <div id="ui-user" class="col-md-1"></div>
  </div>
  <div class="row">
    <div id="component-summary" class="col-md-12"></div>
  </div>
  <div class="row">
    <div id="component-actions" class="col-md-12"></div>
  </div>
  <div class="row">
    <div id="component-stats-detail" class="col-md-12"></div>
  </div>
  <div class="row">
    <div id="component-input-stats" class="col-md-12"></div>
  </div>
  <div class="row">
    <div id="component-output-stats" class="col-md-12"></div>
  </div>
  <div class="row">
    <div id="profiler-control" class="col-md-12"></div>
  </div>
  <div class="row">
    <div id="component-executor-stats" class="col-md-12"></div>
  </div>
  <div class="row">
    <div id="component-errors" class="col-md-12"></div>
  </div>
  <div class="row">
    <div id="json-response-error" class="col-md-12"></div>
  </div>
  <div class="row">
    <div class="col-md-1">
      <p id="toggle-switch" style="display: block;" class="js-only"></p>
    </div>
  </div>
  <div>
    <p id="page-rendered-at-timestamp"></p>
  </div>
<script>
$(document).ajaxStop($.unblockUI);
$(document).ajaxStart(function(){
    $.blockUI({ message: '<img src="images/spinner.gif" /> <h3>Loading component summary...</h3>'});
});
function jsError(other) {
    try {
      other();
    } catch (err) {
      getStatic("/templates/json-error-template.html", function(template) {
        $("#json-response-error").append(Mustache.render($(template).filter("#json-error-template").html(),{error: "JS Error", errorMessage: err}));
      });
    }
}

function setWorkerActionCheckboxesClickCallback() {
    $('#executor-stats-table tbody tr td')
        .on('click', ".workerActionCheckbox", workerActionSelectedClicked);
}

function redrawExecutorTable() {
    var table = $('#executor-stats-table').DataTable();
    var data = table.data();
    // Datatables will not render for display when draw() is called.
    // so we must clear the data and add it back.
    table.clear().rows.add(data).draw(false /* keep current page */);
}

function disableWorkerActionButtons(disable) {
    $('span#workerActionButtons > input[type="button"]').each(function(e) {
      this.disabled = disable;
    });
}

function setWorkerActionSelected(key, isSelected) {
    if (isSelected) {
        workerActionSelected[key] = true;
        disableWorkerActionButtons(false);
    } else {
        delete workerActionSelected[key];
        if (Object.keys(workerActionSelected) == 0) {
          disableWorkerActionButtons(true);
        }
    }
    redrawExecutorTable();
}

function workerActionSelectedClicked() {
    if (this.checked) {
        setWorkerActionSelected(this.value, true);
    } else {
        setWorkerActionSelected(this.value, false);
    }
};

workerActionSelected = {};

$(document).ready(function() {
    var componentId = $.url("?id");
    var topologyId = $.url("?topology_id");
    var tableStateKey = ":".concat(topologyId, ":", componentId);
    var window = $.url("?window");
    var sys = $.cookies.get("sys") || "false";
    var url = "/api/v1/topology/"+topologyId+"/component/"+componentId+"?sys="+sys;
    if(window) url += "&window="+window;

    $.ajaxSetup({
        "error":function(jqXHR,textStatus,response) {
            var errorJson = jQuery.parseJSON(jqXHR.responseText);
            getStatic("/templates/json-error-template.html", function(template) {
                $("#json-response-error").append(Mustache.render($(template).filter("#json-error-template").html(),errorJson));
            });
        }
    });

    $.getJSON("/api/v1/cluster/configuration",function(response,status,jqXHR) {
        $.extend( $.fn.dataTable.defaults, {
          stateSave: true,
          stateSaveCallback: function (oSettings, oData) {
            sessionStorage.setItem( oSettings.sTableId.concat(tableStateKey), JSON.stringify(oData) );
          },
          stateLoadCallback: function (oSettings) {
            return JSON.parse( sessionStorage.getItem(oSettings.sTableId.concat(tableStateKey)) );
          },
          lengthMenu: [[20,40,60,100,-1], [20, 40, 60, 100, "All"]],
          pageLength: response["ui.pagination"]
        });
    });

    renderToggleSys($("#toggle-switch"));

    function renderSupervisorPageLink(data, type, row, meta) {
        return type === 'display' ? 
                   ("<a href='/supervisor.html?host=" + data + "'>" + data + "</a>") :
                   data; 
    }

    function renderActionCheckbox(data, type, row, meta) {
      var host_port = row[2]+':'+$(row[3])[0].text;
      switch(type) {
        case 'filter':
        case 'display':
        case 'type':
          var checkedString =
              host_port in workerActionSelected ? ' checked' : '';
          var checkboxId ='workerActionCheckbox_'+row[0].replace(/\[(.*)\]/,"$1");
          // To present a link to debugging output without needing to change
          // the UI REST API, we must parse the logviewer URL.
          var loc = $(row[3])[0]; // logviewer URL
          return '<input type="checkbox" class="workerActionCheckbox"'+
              'id="'+checkboxId+'" value="'+host_port+'"'+checkedString+'/> '+
              '<a href="'+loc.protocol+'//'+loc.host+'/api/v1/dumps/'+topologyId+'/'+
              encodeURIComponent(host_port)+'">files</a>';
          break;
        case 'sort':
        default:
          return host_port in workerActionSelected ? 1 : 0;
      }
    }

    $.getJSON(url,function(response,status,jqXHR) {
        var uiUser = $("#ui-user");
        getStatic("/templates/user-template.html", function(template) {
            uiUser.append(Mustache.render($(template).filter("#user-template").html(),response));
            $('#ui-user [data-toggle="tooltip"]').tooltip()
        });

        var topologyUrl = "/api/v1/topology/"+topologyId;

        var eventLoggers = (function() {
              $.ajaxSetup({
                  async: false
              });
              var eventLoggers;
              $.getJSON(topologyUrl, function(response, status, jqXHR) {
                  eventLoggers = response["configuration"]["topology.eventlogger.executors"];
               });
               $.ajaxSetup({
                  async: true
              })
              return eventLoggers;
         })();

        var componentSummary = $("#component-summary");
        var componentActions = $("#component-actions");
        var buttonJsonData = componentActionJson(response["encodedTopologyId"], response["encodedId"], response["id"],
                                                 response["topologyStatus"], eventLoggers, response["debug"], response["samplingPct"]);
        var componentStatsDetail = $("#component-stats-detail")
        var inputStats = $("#component-input-stats");
        var outputStats = $("#component-output-stats");
        var profilerControl = $("#profiler-control");
        var executorStats = $("#component-executor-stats");
        var componentErrors = $("#component-errors");
        getStatic("/templates/component-page-template.html", function(template) {
            response["profilerActive"] = $.map(response["profilerActive"], function(active_map) {
                var date = new Date();
                var millis = date.getTime() + parseInt(active_map["timestamp"]);
                date = new Date(millis);
                active_map["timestamp"] = date.toTimeString();
                return active_map;
            });

            jsError(function() {
              componentSummary.append(Mustache.render($(template).filter("#component-summary-template").html(),response));
            });

            jsError(function() {
              componentActions.append(Mustache.render($(template).filter("#component-actions-template").html(),buttonJsonData));
            });

            if (response["profilingAndDebuggingCapable"] == true) {
                jsError(function () {
                    var part = $(template).filter('#profiler-active-partial').html();
                    var partials = {"profilerActive": part};
                    profilerControl.append(Mustache.render($(template).filter("#profiling-template").html(), response, partials));
                });
            }

            if(response["componentType"] == "spout") {
                jsError(function () {
                    componentStatsDetail.append(Mustache.render($(template).filter("#spout-stats-detail-template").html(),response));
                    //window, emitted, transferred, complete latency, acked, failed
                    $("#spout-stats-table").DataTable({
                        paging: false,
                        info: false,
                        searching: false,
                        columnDefs: [
                          {type: "num", targets: [1, 2, 3, 4, 5]},
                          {type: "time-str", targets: [0]}
                        ]
                    });
                });

                jsError(function () {
                    outputStats.append(Mustache.render($(template).filter("#output-stats-template").html(),response));
                    //stream, emitted, transferred, compltete latency, acked, failed
                    dtAutoPage("#output-stats-table", {
                      columnDefs: [
                        {type: "num", targets: [1, 2, 3, 4, 5]}
                      ]
                    });
                });

                jsError(function () {
                    executorStats.append(Mustache.render($(template).filter("#executor-stats-template").html(),response));
                    //id, uptime, host, port, actions, emitted, transferred, complete latency, acked, failed
                    dtAutoPage("#executor-stats-table", {
                      columnDefs: [
                        {render: renderSupervisorPageLink, searchable: true, targets: [2]},
                        {render: renderActionCheckbox, searchable: false, targets: [4]},
                        {type: "num", targets: [5, 6, 7, 8, 9]},
                        {type: "time-str", targets: [1]},
                      ]
                    }).on("draw", function(e,s) {setWorkerActionCheckboxesClickCallback()});
                });
            } else {
                jsError(function () {
                    componentStatsDetail.append(Mustache.render($(template).filter("#bolt-stats-template").html(),response));
                    //window, emitted, transferred, execute latency, executed, process latency, acked, failed
                    dtAutoPage("#bolt-stats-table", {
                      columnDefs: [
                        {type: "num", targets: [1, 2, 3, 4, 5, 6, 7]},
                        {type: "time-str", targets: [0]}
                      ]
                    });
                });

                jsError(function () {
                    inputStats.append(Mustache.render($(template).filter("#bolt-input-stats-template").html(),response));
                    //component, stream, execute latency, executed, process latency, acked, failed
                    dtAutoPage("#bolt-input-stats-table", {
                      columnDefs: [
                        {type: "num", targets: [2, 3, 4, 5, 6]}
                      ]
                    });
                });

                jsError(function () {
                    outputStats.append(Mustache.render($(template).filter("#bolt-output-stats-template").html(),response));
                    //stream, emitted, transferred
                    dtAutoPage("#bolt-output-stats-table", {
                      columnDefs: [
                        {type: "num", targets: [1, 2]}
                      ]
                    });
                });

                jsError(function () {
                    executorStats.append(Mustache.render($(template).filter("#bolt-executor-template").html(),response));
                    //id, uptime, host, port, actions, emitted, transferred, capacity, execute latency, executed, process latency, acked, failed
                    dtAutoPage("#executor-stats-table", {
                      columnDefs: [
                        {render: renderSupervisorPageLink, searchable: true, targets: [2]},
                        {render: renderActionCheckbox, searchable: false, targets: [4]},
                        {type: "num", targets: [5, 6, 7, 8, 9, 10, 11, 12]},
                        {type: "time-str", targets: [1]},
                      ]
                    }).on("draw", function(e,s) {setWorkerActionCheckboxesClickCallback()});
                });
            }
            setWorkerActionCheckboxesClickCallback();
            jsError(function () {
                componentErrors.append(Mustache.render($(template).filter("#component-errors-template").html(),formatErrorTimeSecs(response)));
                //time, error
                dtAutoPage("#component-errors-table", {});

                var errorTimeCells = document.getElementsByClassName("errorTimeSpan");
                for (i = 0; i < errorTimeCells.length; i++)
                {
                  var timeInMilliseconds = errorTimeCells[i].id * 1000;
                  var time = parseInt(timeInMilliseconds);
                  var date = new Date(time);
                  errorTimeCells[i].innerHTML = date.toJSON();
                }

                var errorCells = document.getElementsByClassName("errorSpan");
                for (i =0; i < errorCells.length; i++)
                {
                  var timeLapsedInSecs = errorCells[i].id;
                  if (parseInt(timeLapsedInSecs) < 1800) {
                    errorCells[i].style.color = "#9d261d";
                    errorCells[i].style.borderBottomColor = "#9d261d";
                  }
                  errorCells[i].style.whiteSpace = "pre";
                }
            });
            $('#component-summary [data-toggle="tooltip"]').tooltip();
            $('#component-actions [data-toggle="tooltip"]').tooltip();
            $('#component-stats-detail [data-toggle="tooltip"]').tooltip();
            $('#component-input-stats [data-toggle="tooltip"]').tooltip();
            $('#component-output-stats [data-toggle="tooltip"]').tooltip();
            $('#component-executor-stats [data-toggle="tooltip"]').tooltip();
            $('#component-errors [data-toggle="tooltip"]').tooltip();
        });
    });
});

getPageRenderedTimestamp("page-rendered-at-timestamp");

function start_profiling() {
    if (!confirmAction("start profiling")) return false;
    var topologyId = $.url("?topology_id");
    var timeout = $("#timeout").val();

    if(timeout == "") { timeout = 10; }
    if(isNaN(parseFloat(timeout)) || !isFinite(timeout)) {
        alert("Must specify a numeric timeout");
        return;
    }

    var failed = {}
    var passed = {}
    Object.keys(workerActionSelected).forEach(function (id) {
        var url = "/api/v1/topology/"+topologyId+"/profiling/start/" + id + "/" + timeout;
        $.get(url, function(response,status,jqXHR) {
            jsError(function() {
                getStatic("/templates/component-page-template.html", function(template) {
                    var host_port_split = id.split(":");
                    var host = host_port_split[0];
                    var port = host_port_split[1];
                    var millis = new Date().getTime() + (timeout * 60000);
                    var timestamp = new Date(millis).toTimeString();

                    var mustache = Mustache.render($(template).filter("#profiler-active-partial").html(), {"profilerActive": [{
                        "host": host,
                        "port": port,
                        "timestamp": timestamp,
                        "dumplink": response["dumplink"]}]});
                    $("#profiler-table-body").append(mustache);
                });
            });
        })
        .fail(function(response) {
            failed[id] = response;
        });
        if (!(id in failed)) {
            passed[id] = true;
            setWorkerActionSelected(id, false);
        }
    });
    if (Object.keys(failed) > 0) {
        alert('Failed to start profiling for '+JSON.stringify(Object.keys(failed)));
    } else {
        alert('Sent requests to start profiling for '+JSON.stringify(Object.keys(passed)));
    }
    redrawExecutorTable();
}

function stop_profiling(id) {
    if (!confirmAction("stop profiling")) return false;
    var topologyId = $.url("?topology_id");
    var url = "/api/v1/topology/"+topologyId+"/profiling/stop/" + id;

    $("#stop_" + id).prop('disabled', true);
    setTimeout(function(){ $("#stop_" + id).prop('disabled', false); }, 5000);
    
    $.get(url, function(response,status,jqXHR) {
        alert("Submitted request to stop profiling...");
    })
    .fail(function(response) {
        alert( "Stopping profiler for " + id + " failed: \n" + JSON.stringify(response));
    });
    
}

function dump_profile(id) {
    if (!confirmAction("dump profile")) return false;
    var topologyId = $.url("?topology_id");
    var url = "/api/v1/topology/"+topologyId+"/profiling/dumpprofile/" + id;

    $("#dump_profile_" + id).prop('disabled', true);
    setTimeout(function(){ $("#dump_profile_" + id).prop('disabled', false); }, 5000);
    
    $.get(url, function(response,status,jqXHR) {
        alert("Submitted request to dump profile snapshot...");
    })
    .fail(function(response) {
        alert( "Dumping profile data for " + id + " failed: \n" + JSON.stringify(response));
    });
}

// Create jstack output for all selected workers.
function dump_jstacks() {
    if (!confirmAction("dump jstack")) return false;
    var topologyId = $.url("?topology_id");
    var failed = {}
    var passed = {}
    Object.keys(workerActionSelected).forEach(function (id) {
        var url = "/api/v1/topology/"+topologyId+"/profiling/dumpjstack/" + id;

        $("#dump_jstack_" + id).prop('disabled', true);
        setTimeout(function(){ $("#dump_jstack_" + id).prop('disabled', false); }, 5000);

        $.get(url).fail(function(response) {
            failed[id] = response;
        });
        if (!(id in failed)) {
            passed[id] = true;
            setWorkerActionSelected(id, false);
        }
    });
    if (Object.keys(failed) > 0) {
        alert('Failed to create jstack output for '+JSON.stringify(Object.keys(failed)));
    } else {
        alert('Sent requests to create jstack output for '+JSON.stringify(Object.keys(passed)));
    }
    redrawExecutorTable();
}

// Create jstack output for the worker with the given id.
function dump_jstack(id) {
    if (!confirmAction("dump jstack")) return false;
    var topologyId = $.url("?topology_id");
    var url = "/api/v1/topology/"+topologyId+"/profiling/dumpjstack/" + id;

    $("#dump_jstack_" + id).prop('disabled', true);
    setTimeout(function(){ $("#dump_jstack_" + id).prop('disabled', false); }, 5000);
    
    $.get(url, function(response,status,jqXHR) {
        alert("Submitted request for jstack dump...");
    })
    .fail(function(response) {
        alert( "Dumping JStack for " + id + " failed: \n" + JSON.stringify(response));
    });
}

function restart_worker_jvms() {
    if (!confirmAction("restart worker")) return false;
    var topologyId = $.url("?topology_id");
    var failed = {}
    var passed = {}
    Object.keys(workerActionSelected).forEach(function (id) {
        var url = "/api/v1/topology/"+topologyId+"/profiling/restartworker/" + id;

        $("#restart_worker_jvm_" + id).prop('disabled', true);
        setTimeout(function(){ $("#restart_worker_jvm_" + id).prop('disabled', false); }, 5000);

        $.get(url).fail(function(response) {
            failed[id] = response;
        });
        if (!(id in failed)) {
            passed[id] = true;
            setWorkerActionSelected(id, false);
        }
    });
    if (Object.keys(failed) > 0) {
        alert('Failed to restart for '+JSON.stringify(Object.keys(failed)));
    } else {
        alert('Sent requests to restart '+JSON.stringify(Object.keys(passed)));
    }
    redrawExecutorTable();
}

// Create java heap output for all selected workers.
function dump_heaps() {
    if (!confirmAction("dump heap")) return false;
    var topologyId = $.url("?topology_id");
    var failed = {}
    var passed = {}
    Object.keys(workerActionSelected).forEach(function (id) {
        var url = "/api/v1/topology/"+topologyId+"/profiling/dumpheap/" + id;
        var heap = $("#dump_heap_" + id);
        $("#dump_heap_" + id).prop('disabled', true);
        setTimeout(function(){ $("#dump_heap_" + id).prop('disabled', false); }, 5000);

        $.get(url).fail(function(response) {
            failed[id] = response;
        });
        if (!(id in failed)) {
            passed[id] = true;
            setWorkerActionSelected(id, false);
        }
    });
    if (Object.keys(failed) > 0) {
        alert('Failed to create Java heap output for '+JSON.stringify(Object.keys(failed)));
    } else {
        alert('Sent requests to create Java heap output for '+JSON.stringify(Object.keys(passed)));
    }
    redrawExecutorTable();
}

// Create java heap output for the worker with the given id.
function dump_heap(id) {
    if (!confirmAction("dump heap")) return false;
    var topologyId = $.url("?topology_id");
    var url = "/api/v1/topology/"+topologyId+"/profiling/dumpheap/" + id;
    var heap = $("#dump_heap_" + id);
    $("#dump_heap_" + id).prop('disabled', true);
    setTimeout(function(){ $("#dump_heap_" + id).prop('disabled', false); }, 5000);
    
    $.get(url, function(response,status,jqXHR) {
        alert("Submitted request for jmap dump...");
    })
    .fail(function(response) {
        alert( "Dumping Heap for " + id + " failed: \n" + JSON.stringify(response));
    });
}

// Confirm an action
function confirmAction(actionText){
    return confirm('Do you really want to ' + actionText + '?');
}

</script>
</div>
</body>
</html>
